Skip to content

security: bound archive member enumeration before materialization - #70

Draft
seonghobae wants to merge 11 commits into
security/bound-sbom-archive-inputfrom
security/bound-archive-member-enumeration
Draft

security: bound archive member enumeration before materialization#70
seonghobae wants to merge 11 commits into
security/bound-sbom-archive-inputfrom
security/bound-archive-member-enumeration

Conversation

@seonghobae

@seonghobae seonghobae commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Test-first parser-resource hardening

Progresses #69 as a bounded draft stack on PR #68.

Exact tree binding

PR #68 advanced after this stack was created. No rebase, retarget, force update, automatic conflict strategy, or credential-bearing tree publication has been used. The stack remains draft until an auditable credential-separated integration mechanism reconstructs and independently verifies the exact intended tree.

Confirmed gap and immutable RED evidence

The direct generator enforced the 256 MiB compressed-byte ceiling before and during parser reads, but the archive-member limit was evaluated after parser materialization:

  • zipfile.ZipFile(stream) built the complete central-directory ZipInfo table before infolist() reached the repository member check; and
  • tarfile.getmembers() constructed the complete tar member list before that check.

CI run 31104663109 is the immutable RED evidence on fb4ccefae3899c617bf969a3c93be3924177b23c: package acceptance and Ruff passed; every Python 3.10–3.13 lane failed only the two focused regressions, with Python 3.13 reporting 2 failed, 763 passed.

Bounded implementation

The current change:

  • scans a canonical single-disk, non-ZIP64 central directory with finite descriptor reads before ZipFile allocation, enforcing exact EOCD record/count/size/offset agreement and the 10,000-member limit;
  • streams gzip/tar physical headers before semantic parsing, enforcing 10,000 physical members, a 512 MiB aggregate expanded-tar ceiling, and a 1 MiB per-PAX/GNU extension-header ceiling;
  • rejects ZIP64, multi-disk, malformed or truncated central directories, links, devices, FIFOs, sparse archives, unsupported special forms, malformed tar framing, nonzero trailing data, and damaged deflate streams through stable fail-closed paths;
  • replaces tar getmembers() materialization with sequential r|gz parsing that retains only the bounded seen-name set and one root PKG-INFO payload;
  • preserves descriptor identity, no-follow opening, live size/read/seek bounds, digest bracketing, metadata validation, dependency-lock parity, deterministic output, and stable public error boundaries;
  • normalizes zlib.error without traceback leakage and locks MAX_ARCHIVE_MEMBERS to the overmember fixtures following CodeRabbit review;
  • includes scripts/ci in the repository production statement and branch coverage source directories, with the repository contract test bound to that exact configuration;
  • preserves the protected-main or organization-level reusable-workflow integration boundary required by the release contract;
  • adds focused regression coverage, beginner-readable operator documentation, APA 7 primary references, and one [Unreleased] Security entry without a version bump.

Contract-repair evidence

CI run 31121580673 completed with failure on exact head e2c3db6850c055f6ae5f904e00b1ca389a788cd1. Package acceptance and Ruff succeeded. Python 3.10, 3.12, and 3.13 reached the suite and failed only two stale repository-contract expectations; Python 3.13 reported 2 failed, 778 passed. The Python 3.11 runner failed during job setup and is not test acceptance evidence.

The two reproducible contract failures were repaired without weakening production behavior:

  1. commit 393a125171dbd797a6bfcdcaafaf83b814d86aee binds tests/test_quality_contracts.py to the intended source_dirs = ["src/egressweave", "scripts/ci"] statement-and-branch coverage scope; and
  2. commit b8526736db3bb16ed88290ca64fd07cf54232805 restored the protected integration wording required by tests/test_release_contracts.py semantically, but line-wrapped the literal contract phrase.

Pull-request CI run 31125435331 then completed failure on exact head b8526736db3bb16ed88290ca64fd07cf54232805. Package acceptance and Ruff succeeded; the Python lanes failed the release-contract assertion because the documentation split the required literal phrase protected-main or organization-level reusable workflow across a line break. That is a documentation-contract failure, not successful acceptance evidence.

Exact current commit 56c9029588b67f329c334b4e56e1361087f8f651 changes only that documentation wrapping, preserving the implementation, tests, standards references, changelog, and protected integration semantics. No pull-request workflow run is currently associated with this exact head, so no exact-current-head CI or security success is claimed.

Review state and remaining gates

The prior CodeRabbit findings for zlib.error normalization and the archive-member constant are addressed. A fresh exact-head review was requested for 56c9029588b67f329c334b4e56e1361087f8f651; prior-head reviews, draft skips, and prior-head green checks are not current acceptance evidence.

Keep this PR draft. Do not merge, mark ready, enable auto-merge, release, publish, rebase, retarget, force-update, or infer success from missing, queued, failed, prior-head, or draft-skipped checks. Require an auditable credential-separated stack integration followed by exact-current-head Python 3.10–3.13 CI, Ruff, compileall, product-guard self-test, 100% production statement and branch coverage, wheel/sdist package acceptance, applicable SAST and Security Scan, completed automated review, independent non-author approval, branch protection, and every repository policy gate.

@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

ZIP 및 gzip/tar 아카이브에 사전 구조 검증과 리소스 제한을 추가했습니다. wheel은 중앙 디렉터리를 먼저 검사합니다. sdist는 tar 멤버를 스트리밍 순회하며 메타데이터와 특수 형식을 검증합니다. 관련 회귀 테스트와 릴리스 증거 문서를 갱신했습니다.

Changes

아카이브 검증 및 SBOM 생성

Layer / File(s) Summary
검증 정책과 릴리스 증거 계약
CHANGELOG.md, docs/sbom-release-evidence.md, scripts/ci/generate_release_sbom.py
멤버 수, tar 확장 데이터, 확장 헤더, ZIP 구조, 특수 파일 및 릴리스 검증 조건을 정의하고 관련 상수를 추가했습니다.
ZIP 중앙 디렉터리 사전 검증
scripts/ci/generate_release_sbom.py, tests/test_release_sbom_member_enumeration_bound.py
wheel 메타데이터를 읽기 전에 EOCD와 중앙 디렉터리를 검사합니다. ZIP64, 멀티디스크, 절단, 구조 불일치 및 멤버 한도 초과를 거부합니다.
gzip/tar 물리·의미 검증
scripts/ci/generate_release_sbom.py, tests/test_release_sbom_member_enumeration_bound.py
gzip/tar 데이터를 스트리밍 검사합니다. 숫자, 체크섬, 확장 크기, padding, 링크, sparse 및 특수 형식을 검증합니다.
sdist 스트리밍 메타데이터 처리
scripts/ci/generate_release_sbom.py, tests/test_release_sbom_member_enumeration_bound.py
sdist 멤버를 일괄 로딩하지 않고 순회합니다. 루트 PKG-INFO의 경로, 중복, 개수, 크기와 tar 오류를 검증합니다.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant generate_release_sbom.py
  participant ZIP preflight
  participant zipfile
  participant gzip/tar streaming validator
  participant SBOM metadata extraction
  generate_release_sbom.py->>ZIP preflight: wheel 중앙 디렉터리 사전 검사
  ZIP preflight->>zipfile: 검증된 wheel 전달
  zipfile->>SBOM metadata extraction: wheel 메타데이터 제공
  generate_release_sbom.py->>gzip/tar streaming validator: sdist gzip/tar 물리 검사
  gzip/tar streaming validator->>SBOM metadata extraction: 제한된 멤버 스트림 전달
Loading

Possibly related issues

  • #67: 압축 입력 크기 사전 검증과 같은 아카이브 검증 경계를 다룹니다.
  • #69: ZIP 멤버 사전 검사와 tar 스트리밍 멤버 제한을 직접 구현합니다.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed 제목은 아카이브 멤버 열거를 materialization 전에 제한하는 보안 변경을 정확하고 간결하게 설명합니다.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch security/bound-archive-member-enumeration

Comment @coderabbitai help to get the list of available commands.

Copy link
Copy Markdown
Contributor Author

@opencode-agent Implement the bounded GREEN repair on exact current RED head fb4ccefae3899c617bf969a3c93be3924177b23c of branch security/bound-archive-member-enumeration.

The immutable RED commit is valid. Local execution against the exact current generator failed only the two new focused regressions: the overmember wheel reached zipfile.ZipFile(...) before the repository member bound, and the overmember gzip tar reached TarFile.getmembers() before that bound. CI run 31104663109 must remain authoritative when complete.

Modify only:

  • scripts/ci/generate_release_sbom.py
  • tests/test_release_sbom_member_enumeration_bound.py
  • docs/sbom-release-evidence.md
  • CHANGELOG.md

Required bounded implementation:

  1. Preserve the exact 256 MiB compressed-artifact descriptor boundary, no-follow/path device+inode binding, live read/seek checks, remaining-byte-plus-one-tripwire read cap, digest bracketing, standalone API, and every existing archive-name, duplicate, link/device, metadata, dependency-lock, deterministic-output, and stable-error contract.
  2. Before constructing zipfile.ZipFile, scan the accepted descriptor's canonical single-disk ZIP central directory with finite reads and no ZipInfo table allocation. Count actual central-directory records, stop at MAX_ARCHIVE_MEMBERS + 1, require record boundaries/count/size/offset to agree with the EOCD, and reject malformed/truncated/inconsistent input. Fail closed on ZIP64 or multi-disk structures that are unnecessary under this exact 256 MiB/10,000-member release contract. Rewind the same accepted descriptor before the existing ZIP parser. Do not reopen the pathname or use unbounded reads.
  3. Before tarfile semantic parsing, perform a streaming gzip/tar header preflight over the same accepted descriptor. Retain no complete member table; stop at the first physical header over MAX_ARCHIVE_MEMBERS; enforce a finite aggregate expanded tar budget of exactly 512 * 1024 * 1024 bytes and an exact per-PAX/GNU extension-header payload limit of 1 * 1024 * 1024 bytes; reject malformed/truncated headers, sparse files, links, devices, FIFOs, and unsupported special forms through stable non-leaking errors. Rewind afterward.
  4. Replace tarfile.open(..., mode="r:gz") plus getmembers() with sequential r|gz parsing (or an equivalently proven bounded stream design). Keep at most the bounded seen-name set and the single root PKG-INFO payload; stop at the first over-limit member and never call getmembers().
  5. Keep the current two RED tests unchanged. Add only focused tests needed to prove: exact new constants; valid canonical wheel/sdist compatibility; lying/inconsistent ZIP counts; ZIP64/multi-disk refusal; truncated central records; actual count above the declared count; tar aggregate expansion refusal before payload skipping; oversized PAX/GNU extension refusal before tarfile allocation; no getmembers() call; stable errors; and every new statement/branch at 100% coverage. Prefer compact synthetic fixtures instead of large memory allocations.
  6. Update docs/sbom-release-evidence.md with beginner-readable operator guidance distinguishing compressed-byte, pre-materialization member-count, aggregate expanded-tar, extension-header, metadata, and residual mutable-storage boundaries. State that canonical release wheels/sdists are accepted while unnecessary ZIP64/multi-disk/sparse/special archive forms fail closed. Do not claim parser immutability, provenance, signing, attestation, publication, or a SLSA level.
  7. Add one concise [Unreleased] Security entry. Do not bump the version.
  8. Add or retain APA 7 primary references for MITRE CWE-409, MITRE CWE-770, and Python 3.13 zipfile/tarfile documentation using the repository's existing reference style.

Run the two focused RED tests first, then all new boundary tests, the full Python 3.10–3.13 suite, Ruff, compileall, public-docstring checks, 100% production statement and branch coverage, product-guard self-test, wheel/sdist package acceptance, and applicable SAST/Security Scan on the resulting exact head. Keep the PR draft. Stop without writing if the head differs from fb4ccefae3899c617bf969a3c93be3924177b23c. Do not modify .github, workflows, dependencies, credentials, permissions, protected refs, review-agent identities, release behavior, signing, attestation, publication, tag, ref, network behavior, or any path outside the four-file allowlist. Do not rebase, retarget, merge, mark ready, enable auto-merge, release, or publish.

Copy link
Copy Markdown
Contributor Author

@opencode-agent The earlier implementation request remains authoritative. To avoid any ambiguity and connector-size truncation, apply the exact verified four-file patch below on exact current RED head fb4ccefae3899c617bf969a3c93be3924177b23c.

Patch contract:

  • gzip bytes SHA-256: 75ae2009763226b18e157b1d1a1de9be3eeeadec8da759b8692b01dc7d254dcb
  • decoded patch SHA-256: 61aef2b19e071c7135070a80c33d3f19af7bad91f99463445ab0fe1a47556e0a
  • decoded patch size: 57024 bytes
  • paths: exactly CHANGELOG.md, docs/sbom-release-evidence.md, scripts/ci/generate_release_sbom.py, and tests/test_release_sbom_member_enumeration_bound.py
  • candidate Git blob IDs after application:
    • scripts/ci/generate_release_sbom.py: 350bb3475896079a1d102eb672a4c9747758cb5b
    • tests/test_release_sbom_member_enumeration_bound.py: 5838402277f96ae4f60ff75f08081ba329b78a30
    • docs/sbom-release-evidence.md: fc5e8c68fc8fed13268f8cf4e5a84442884ef73d
    • CHANGELOG.md: 7400789eaf8c19b82364d6668c626c7f9394c9c8

Procedure: copy the base64 payload without whitespace to a root-only temporary file outside the repository, decode base64, verify the gzip SHA-256, decompress, verify decoded size and SHA-256, run git apply --check, apply it, verify the four resulting Git blob IDs and that no other path changed, then run the required focused and complete verification. Commit only the four resulting paths. Do not commit the payload or any temporary file. Stop without writing if HEAD differs from the exact RED SHA or any digest/blob/path assertion fails.

H4sICDOUdGoCA3ByNzAtZXhhY3QtZ3JlZW4ucGF0Y2gAxFx7d9pIsv+fT9HLnLMLQZIB40c845wlNkl8J7F9bGeSGY+PLEQDGoPEqiU7ZO6ccz/E/YT3k9yq6m6pBQI/4ux6N4lBperqevzq0dIMguGQ2fYoSJi3cfCue/y29/7krTMdsH7hYyUIB/wL29nmm1vDLcfZ6TSbO7svWavZ3O50KrZtL9xfaTQaizz++U9mb+9Y26wBf7c2GXz2BmMec8GSiF2e86kXJoHPfuGxCKIwCEdXtXGSzMTexobg01seO1E82hAz7m/ctp0m/G+cTCd1p8IYuwuScZQmzB974QhuZcmYM5+HSexNJnMGrL0RH7CY3wb8zobfQ6CN+QD+DbwJ8yOk9BPgVWE//PADO+d+GgfJvNKw2esoDQfMi/1xcMvZlE/7PGY8TKc89hKQlPX5MIo5m3mxgCtTL+ExMA2+0lWHHXghbMf3JpUGCDrmfCJYGN2BMD5Hjh4bBmGQcCZA8Am3B4G40bLDByBLonjOZjEfToLROFHrIbfr34LZm2DCrxlsM/JhZcGCBBTq9SfcgsXgGhNRGvucAdskDvopCiWMxVtNq9lsIrfZeC5QTnvMvQHsZBJMg8QCkq1Wm30IXjNvNIr5CFZh/MvMA58Y2ImX04GWWkiHvE67nzfeHn8EwoSHaE9JJbSyBP9XqpR/Hf/36Os1qQ8U8CP77eh0u2OxaTpJAlKGhQx56MfzWcIHFhOkaQs4hjcWG4BNffj05ujNCdzlTYD/FMlQngj8IGZpGMJ2hfDiObJCF8KFkVCwoReAA0wiAf6h3cgbDNCJYj7hnuAsitks7U9AN2RvLwUq9A5wF5udZnbh4KNzJi2m77XPX598kFZHNsoW2pk8waKQA+0onXgxOvIQLYZi5G7R3tom7fvRFHxAgJx2f56gewcTlFJpFNSGK4y+BrONRDJTKgWt8MQbeIkHK3kDYSGdFyfBEFyejT0xBiKHnc+nqFG4rJ0u4ELrV1iVgYkWg8gXG6IfTW29USCDcPK5hI+11xWe7Prt9ubm0HGG/hbf9beLeLKegwSY9TSIOO1dq/WSNeCfzRZCDukX1J5CeKUC3IkF4QwtDs4SogFZMJ1FMThqb4TK/sS9W+6wowT8USR7FbbpyECnO1B3YEFAmGTsQWASUgy48ONgBvqz2A3nM+UXEh7s20AEEJxkCeSBdoJo4DfgzrfcJhYgVX9OECa8qeEA0uAW6wNW3fAkN6syNDkOMVSuc/6ua+PdyuwoMrl7MCTu2g3RnYTETsAJAD5gBTIHQ+XxP1bsjgNM/kDHTkPhDSkmBumMYgKhLxkLGZCGx0DwY9QZqOnD7hJwJRtE9Kb9YJRGqci2AX55i+D/Ffaff4dCZ1GtRRYg0pYDInP/hrYy4D5EEIYwhVqmGeSmIwRoVAjJPW070gr8C8QB2BBDUd5+/aF30T3sXnSvcZ9xFCU6cq9Pf35rHx2/OblWewI+O47UFtrYv4HkwgJKLMkcVQJILggw1cJyRyjJhCc8gwtQTDAFUAdFK3GvIZLRXZ0M5CFNAYanfiJRXilVgb3wvTD3GfIjBDvDHf8BiYHH0yAE7CvJNSzPNQRAyoeU70jrE0dcZBjEIskyBkMOc+b1wYAqoeAd/0qBo/Q0P0kpz8I2af+EYDE6nGQZMaAGCiTunRwcKoOS+5meq4AaUNhA+jw3EDMjc6D5KJ8wqbiUyg2MVV9n5QzlVWoeRJCdEwADPvix0tjKzQGgOiQzzHgsU4fQNUsGtLAK96a5GXLlw3dxlI7GJGGG7RqtdcrNkrxWeJmyFS1pm9hpjfMQ5ML0cl/G1qkD1erJpK2/I4bAhnHPH2MaRxUWM/nMm08iT2VYJeaanCwNDA6OUDEgkwCGpDOEWTBlIRuDxrc1wJbWLXndMJln6fr6wosxQpwRT2RQiFqdQtejxbNgiwEWgpDgJQ8dWHPnIej2QMCSBl4BWo3dbwStxsvnAa1Gq/mMqIUpR4X7FEtrM3jBB0HnQnqyLMBRXV4YDLlIAD53M0FUAcVnPMQkPme3shewdCKTUvDwNoijkFaaevENxkK+Bmyt9cwccXv8C7QE5C+U9W0ot2/Qe9O+gEwchFDG5goQth84yZfkWtfg0rswvBhBciYPRFXsaUiK0skAoTK4lW5HZoESMU0E2KaowFHszcagPfCHW2g2BniHMiCVbeenh58zO8oLwwA2b0EpG0/gH2IA/IbQgoVU4DVa7edjJl1iIn1gHMwohQCkeH2AGfQxwh5/7gNb+Aw7QY9EB0sTlTN0haqrGB3uVLkhfGESREgOAWp/pGLKRttzaDTA9QldPl68sXfZASwDgQKbaDk77L/OT45zZuC/IvGmM0FRA0yiqblFB9Sy+e2CNVqdZxcMetWLcRaL0NNiQwq0IaAO9p/gOtNAyMSUQkONSKuLhfm0H4HmbYXXOvOD3Yr9lEA3p2w0m3h+sa2CBGqr1iXXCZXT2NJT+Q0Bhg3/rtVuYvXd9X2OedomhNOwCMynEGwYSzLlRdjOKqiyqWawCIRletlQElYywCR2VhEqLQPO8niT4g8CCKtEdv3RRPwog9zor2RPIIF6EHFBFYFSARBHGAWh5opwN8TIIJM0wCRwWXW7PiYIhp0TBgaUPoCfWVkN1QLGt0NW1EU+3SjLN1FpyMqcAH8YR1MFAbJe0SqX98scsGZsAMyIKQ0NjqBSyIYGxgwjHygAtCvJsrrDqFNIyozj6pnIYoUkOerqxKxJKg1z51QYFbWloRRVICsWPWMo3IgYU2ksFSvLVVwQQjEM2qFiWspl2lQVRTRoyRKzXAliHNzTm4Fwcni1lH/RyZZyLyBJpZENg8wK3KJQyqpYXYlSxs3oSSOSaaWxOMyh+PFUdDklI5SV8xPgVT5AyQu+FaOUrHLTbSRVcJWGUaejWgPQL+6FFB6iwFS7hV4c0xBM9u0ZfuhRHCAeLlhpQGqErSl4m9iQcGaR0OABq5MdVe8qe2xUNNmTaiAMa4BYMGkKIQqcZPTb4MYg9IBdy/HBEHI1eMMtiggInoVsJYszIUNQjD1qkPWkIcMZ6UNGbpdJMRDksgDAUVjRVRKmvjgSUM9mdfDpHIAlZJtOq6l7BfywqYssiMaDKBwEUoOErq1Oy9pljdb2jvUS4VVCj4Y1nGIhnrEInFZOOnB3iPK58HJkAT6MMt5BDTTK9gsAq3MYlTmwx1QGImKxhkOqmfo8ueMcTUw5hoZkwFa3RZWs3LOx/koFVU9sACVagqkpq4OXCiP0YVl05ZdQiSKaKFkgPAZctrUQE4A0emuWruV1mrGWER4ZUUDZ/MvYw+SNLLNwX0goANcTag1IT/ZdFN8MJ+DDHmgXruWtasU2x4UKeahgL4xVHMSpfNll7I5kPlyAQd0zSviUomWxaoDVwm0IZY/dWqWxtDf2wK1BcXKYN79ma3GbjcolZBdyshptYYARhIBMgyDEwS22zKAdIaeU0gnRN23DEdHmaoJmjwBesGRA5kFoy9yN3kSSwzJcyCDa2bVaO6zRbm5ZbQqjnj/12BHCSujJaLPY39nJp+75KXuD3NVgv9ZutrfqDnuRV3EEiVl+h5oO9fA2SN6lfaAPnYGD9B9pWJdBnqljmoQIdPlAjAGQoH4IPQxUUHSF9dNgMhDOC8WSHUY+wMIZh1zAEcu66Qi8iW1ZDESD3gYBq2LrUxScljojKC9BGLD+Bg83PApWsTGO7uwkEhsCzz24PYeoICfYSA1stk1BV1+pNL5lwUrjKUuaWsato5b1UpLwBbs8kDU8TuGHyR0AzpWj76owLbMhbpEBrGF/OLo46xlLHHzq2Z1mc499DBXqTuiUSeV+nNel0xl5ywtA7+kUnOIT925CiLuK3cvrJSdb37/jDuB1zOm0C2MV6l2aFpEssBode1UaJAswhXQYGw75SLlesBoQVxrq1I11nHaz/gRpHiLPyz12BJgTzcAEkK8HNIKKhmwM5SlUBDk6Q02CGFVbhrG6lJcVxc097l55Xz5Y3p0d0F83r5BBUK1AkTVpqsyPqE6JoyTBPX2bjLBuJqOqCc6Vvy6hDwmrKtr/+5//PVNnCoDiOFxMqC9TtRlQiBeVRi0vM+D/Hehu/BSRU2o3NzyF7oxoSUyk35gEAM/xfEOtKMWEsFgrpgY9Nc0GMT9BnMsRM55Z6fS8V7EPzaTEZkEyhFIR4U4LXRS3BPm21yCfuZtsK3rGjlv5oZAUbb3+A81w//4a5fsDd1lvlAVAXWWVh2wFcFKtdUrTJYzAbnacmRvroFAsFLJabo+KweQj5vO3KeT5e/NR4RxRVghiww821ACDu6oncPFgD3bL+g+hUmeKQ6+/y196jrO51ez3NzvFM8WH8JEniw+hxOoBaoc2a8DfHawcfKxrs85iGPAJljciUvWPT+0KzUcABgepL4/e7PwqToAgGVGj4brDFA8sXFedSUJoh5GR8fS38YjqqUpDfYEFYnYVh5XgIdnnIMp+/UOgDdWHSGS/Qsurf8UEm/GVRyjZNYUC2Wflf0p6bEknzhRbRRwtS5IP8iNpbrONo6HNjtXaQtUd/Hrw/uS4d/jZPT/tHbi/9M7Oj06O2T6rQhVVrbAP3c+ubrDd179e9M7hGn3ZPT560zu/yL5suc3Orru1sy1v6p4dvDv6pQc3f3gNTJGg6eITD3T1rPe+1z3vAdXF0ZvuQc4F5zEvgLTdUf9AxgD63ufT7vFh79C96J5ltDjPKKFFkt7ni94x7iQXb5EUQMLFIy/3/Oi3Hq7clt8hi4OTDx96x7lU21vu1uaWvH4AF8667913ve5h70zf3dmuNEi29ycHP+svQUD5bS7Oxa+nxBGM9RWHV0mNDi7YnxrcP787RBqLZV+8fV/84u3xR/zCBbu9Pe5+6P1VadQrDH9zz3un3bPuxQnpGzIdghHcUourl7brXDWqQHj+rotKXrzetF969vDqz+3OX0h11vvlqPcJFI4zaPf9EewazHV0uCA7nfVDBQ8u1W5tW5tNesAI8ipzaYznKhh2sWUQNfp7D9K3SC7Bra8sNVnZQyevM/sVO4aCfo+m2PQTewE0I+dzkfBp70uQ1IbVP+Utf1HDjYcFOOQo9pzUpOAucDBIwuAoz6V+qSbPCvfYa+pvjnDYGnwFCQLsY3gcR7GrwieXiuaBe9JS1Wr1zDwJur7G+6+v1XF+9iwLTYWo4chP4TIBodWEDssBXpKpP07DG60ZYnQFir68klfloAl57ZOw8lt9xKGuKfEybkhLW3Vw87WMrp7TBUOasBK5cXup4guaqS+sJRxvhnOCGn0yruaS2/tswsMCQcwBZ6GbrVadP6JAXRN1rMCU2QDbXJpIuDhEwo/bnRp9sSfVLa0TRRPTOMT1bsxpcuZlR9A0/6JzluzonVjJhEEjIug3hJzi5aaJhkM8atpnTVPt6tufaFPEpr5XUGz+PbNz6s59aq4WjsVxioUW8pTcRmFTNbRMG3CDgaV+ozH/vtq4k4Z4pOVieqhVf3r3rmrJfVtKLIORkrMBeFbYjMH2VdnOvsuusnWBZB/U/wWyR2txJWnsizjlZbvI5S543Btvgnm76GaRP1jCBnKvJJ1N+CXBQ+GvqyWf88umzCUDJmOsifO/7OkQTEe542noVNZ01QzTxVGOq/txN3/UQUmvNAjaKzD4iRUSnqHHb7cWwPBEiwk7qZkLW8V1WYOVJlnFSSEWPphV4AKeli1iLCrVsgDtVk5qsUfvRQSj0MP6D3j3q6c///6luQV/tjVQQ58nD2r3aRkHcjLgXnZX3QSJnPgV+O8yPiADDKIC3Soz4U+t+JEOtcHF3DDFkyBr+aryPTd7nrPkZiQJ6CB68XICZe9kzXXNnVS9+rKMxzKCaIr9Vtn99VX41XnXPkIIQ91ZhuYarFMvsgAlm5cX3dBcHPElM8jesqDAKveLIKTFL1cz37sq4fF8wGhIVStfx3AK9rcscy3+RHHBP9ZTmq6ClAXfWL6r/lANlJMRrhZ1I4+RwsJDTPlJG+kqe9isWs71UTosbFAmoDfwc68qtT9J8gfdokuM9TfVn+hUZYpbeDivzLswIWaSsVVgjIGk46BUuwt7bBT1BI5krPNviRpVAxSxbRGritBmymhUvvekAos1DYDSNe837SWrWLLHC1y6zdWP35UWMLKn0rWKeq8C/mTPHBlvbRhPo+qj++zhhOLrDnmhos8RH6pNF9P2Qs1Vkv+LTOrLrZDJVZVM9LxrHrTFmr2sVQqGBktZI5U0+N+lvlXPdBRLmOJCqqApfrlCxgWqh4tk3FisvqWAl3udK4xTVQy14E+7+l0Ukp9EKEeBrrn41aqi4OgIKgIpLs48zdZo4o3Eyl7IuGu3ftm8yu/DSYUOfmpA5e+FikXmRJFALV6+woJFoHYxxdwt1TsGWJ4RVmzXYNNpFwWXkwWtrVUDq0a+Qfg93+FCXbQQJznnV6XB9HyusBDI0Mq1CrIsXH9VNnK8VzApEL4twQfCfDPDVk9W4Uwpmcvz8oXO1DD934q1/VNT8spaZqElJof++4p2+Anr5s8/rVt2wZeX4nJ16ULPnT+OvhgD5aT1Z9j6mirITEQGFkTCOe/1fnYPPp4ZxHKM9GggN4Dl2bF7zejs36i5IlyWK68wIjQwJh9jFGMdwm2x4HjEMGN5GCPfoZGvyOBDc3h0LwKBD81VS6qSplmGyRN97uonKl0ABJcGkzXzTmPYrIcMeuIsP2f3y0GzceWFlU2uQEt7NOsET6MJFlxaHFDJ0XFe7B1IAel1BfXui16LzqljPtJPjUWz7CFBuRg9Tq2e48yLPWqFKQOsOJexF3ezzjglL5sUALnw+kwZHJePxvXTp/ukEC+OvXmtLnMY7YyD9qgsfsokHSdcGa0lz4a2608cq2fuWaaJhcCnR1FBEQvQrDerqI1qX/9oI9Ljao8c0i+7J2bjFaP8IqFyx5oWEJC9Xy20MboDcWfeF1eRaXJzwl/sYH7RfYt+axNfl5LIIZZfjrLp3dPsoT3V4OCjAY+a8SupTPAE/j4WWVq91P71q6xaMlnHsCHyn/Y1X3plINF3X6oWmKiunEAMghH4R/17OxCA39JM3awgAVZqpSIW/APPuhjYJeU9PCfCV33pwOg+6Zfb8yftZ3F2KI/CUYClwhgCgGUmaCwnnJJiF0wmbWfn97Xp7ZCc5atVTvJdjCYXzl3vUsrXYC22Z0h1tYRJ6twRrgk8qARv/T2s1mVk7leJBOBRUl3u2a2r772TGz5nOu07YjYBliiJxVrFtgbkB1KHKm4t+tvjj46McUfugZhhp3oOjcD7XxGuJ2jB+9vVMukXzpeNNyfpPUnZI1SXT8/2DQuUjmwELvP4kQ2pL8cxeijYeEdEdYRi5TsgGdotljPLMEBPUeF6ztuvcvhTQxSN+n/s6zOWaTTg+9W4D5rHYFfAv7dY4RYSx9J8WerAlcXX0tWvPI7cPj7bU3KrhGg8+CuZGuKc6g6f33VxJy7YdHYX0JHOA4sWDKVltvKF4P2MNq8Hio9+WKsEqJdPoInviuGnOioikvqTqq5vGMGT/z29RlvNuF5+qbzQkHt/yB2WqmXUXO3e8nwdR2s1ycKDPrYhpXU/YyXqGgNRkbqPvm09+CADfyhW8AEd8tNGroyS6T5UwHvrufSr1VJvRUgnmmc84nl6sl9/tqP2gpv5vVllLxZMt2IHJvAUR08L7E3Cn1h7TdBhMgnClJdTrAWz+0Htngj+Jsy7x+Vk7vDkf9egDBlle/QoRFzwNc1+b/0u+7DkzVpmdMKrmD0PlD4BTr8bpN5jpHJozdSxVnGaykFxZzWKpfp3UMvzocF9QFnuK8WIfvRp5BNL4NJiqBx2QMAC1UPm3k+B5fv8c92c/MHwvNx5FjVBx6rykdcLL8ZXpx3s6/rpcI3oZLp1STj0I/yPjO1X02Ro71bXkWL/KParIoVf8D/mwoXvzXj14TlZ9cU1vY13VKFTh2wZ3XJ9Vbv8H86opW30kis6yXxGD8X8ueI5CrX5818//H97V9/bxs3k/79PsVjgACmWFFt2WteoikufOpegaWIkBR7cubnFylrbQmTJ0MqJ3af57g/nheSQy32RIilycUAb29IudzkzHA7n5TeYx1x91es3vza46h8v3zW46ufXTcaC+usGl8ms69ev3vwauPxLY41RfdzECnHMucES8ZLUH0n9waDwnu/Pnr97f7ohXdDsaLyMoSZnw16IGnl6d/rfDdj2vNllv7yqEqgngRz+r+H/2qhNQACrk5szQ/cfgSXfqtoqjA4KMCp8HyJFCSLQThooHdlMalt4g9XgGaYCTO65q22yt+loZJMnSl6pJSmxVzzeHrSjp0+9jwN0KRyvgslXEH8ZFPaOuvKXL6Hd1T3626DP8gf/mkO/oGKnkR+iRELoYE8/AteEJZ9Df+EhK0Mo5ydSxrXHnq0SK6h+hM65rPX2PRssp29f8G/CLCs3YNZkJxeMEMQjmEjTMRCepRIk3I0SvZq8wFKx9ojL5Lj8KI7jM9zNdJTJQXJAb6o8WGrXP2sSt2buhAqlDo4630d7z/p99UOXSa38jjxHnf/H9we9yYWJYQETKpXZxE6QIuT6zbUVbsoUhhKQGeffLfMm1+Qntv/DHgAix+fsYW/q60G++MHiDqv/c5uh2hurgwKUUbXaRPTDw07/WFH96KDz3b4h+3JkY1prx/0aaI0YORqjaE2kDsUVJKm7Dqn1Ip7dovMy6N0/ufozdonfrSa+RIJ0rw3WA56PlXLoIdQNYF/CX7Ab8BAfOsGNMvZGhhSR6UMLhxrn+cNNC4NB/Pdk+tH5W9nU6u/A09re5NZjuTtD5koBXoDlNojOiw+D9yl+GnjTwK2XenrASJ/02t4BNXV2N8/OZvn4/ixdXLcM+du9WwisteEk0Q/fXXonsk/dF2txjt0BPhTYBS+iadGGhJ4DP/bURDj/Kginm3KVZVOlNzOqNIUcjgx0wl6IIYmrdqO/MACn7rE5IiF+hB/sTDKbtqGo5xG7abRsIZsBLhDouuGDjgWBlRW9efOXhpfsKaNNyGlpLCMBujiKSdy1piN9WYXyXvlKbqTItupx0C+GJkur/Wh8C8DklqMfJRnH81a7/UhO7ogANyhVxcscnN39ovSMTKbt4trZH0qvxisD+0HjMzSkSXk6uSKvbd1Rbeg1oHlGHSKklbZ8OFuvGeFh8M8Fm53Pin6Eygg9wkSiDaONPv4MRQnmW04Ne7di6jYYqglAGM8YuSZo/eVnXpDLgZ0OhTsDWCl70UG7Mq/DH7X9yOWkZAF7sB7L7VM1y7J4HNBvcL7/oWrtrWju11PLe6O6NSNetziXwJL52vcuLgbvsZzN6/smPHFvlx7P9hoM5ktJ6WAykuY7noTfad3xWel3ilzwLIAlzPFfB5uKT96JwGNOUB4ITGuFuzS41sFxlvXTXu/Z8eHx0X7fBddaZVwC21rlToSQAi/WIWLfLomZRT8m42FPUX8SRs4az1ZDvgLbQ42svwTTCN/2+x86h9He8UHn+6Nn2v2Dc5buEppgAqDjiXrxxHYTyxLh04CpmDPqLRwVFz2Ut7xlBQ7AnBcX14O4sppMSKyBkO8h0CgyoUVvR/DrBmvr7PnvL2X+vFlNYZQiAiVxMOXw1XSnNtPVgLGODV6wzRXl1St2lfg3Df/OSIsnUb939MdU7BHxG2UCnkQZtnL6DK2c3K/Njfu9w96++91ranfQPTU9ME6i57fpxXXW7fvXviNElLz7i6LVCYIo3v802O/1jzs/qn9/qL0aaPPT4KC33/mxbvDxaJr+NDjsHaix1WzNte0ephSgEW34gkCQ5GNE6/kExbETPeFqUu1nwCQ7U1XqfBpIA/6HUrsLquWBejqE7TaIM+S8vRzfI0YFCqiu6DG13YSMJ2p5ppcz9TThdIUsi1YsGNdFFvVAGLtw+VO9iWo1Dx/2dAEc/hSf88SgVpt+4/KKkKuXRJ18O59jixqtpjAw1746S97//vbd6S8ljh+9vZY8WF6CPFIapgVv2pFryUegorzuXHBSlF2ZAqMqYCCNpg2le8Tj28ldTh2XsMcSJmzq2jQuDOfHWnaFqprwxIVlhxTe0vl0UFovSlPmXJsi8Gz4wjTPM6Uu8fqfTJa1lpiKAmSzb+OtytD9zmbs86A6DKVHOzHj7kVHHygtUxSVOxrHGd4gCBTW2Dz9nMD2ARE+Z6nVVhH9c07VcLCMnJo4YpDi48xZai7cObUFIH+8YRBtC8AR9FvGnzk7fpRBfxLcDeuT7OH4PACtwEthGHci7QEVA6nvQWUP9itcoI6omwiVoCGERUnasMgVw0yopUyBIiTedyII0yZWPXkpGBWbDxIQHP/53U2XTKxAzYIBzQtWHJZlg7WEz0MGsGUhoJMkQ9NwvkAa2uIClj191wxyzaC9QroYiPya5MXbd789/93B48AAcah0zaPLGdTBTd3uHmZhQwcW2+zp2UGfOk76YIG33CdzELW6MvTYjv6TICeLy0gtOJ0BzXc7L3+f0DNaH7MHFoJPkJ5WioB4insesddsQVCGpwZ4irfqGiWUcuCrMvgv1RZyoRQfOEDVi1+phWImNZyNoBLoMv6XGuLL4F84yBe1zYr9tVidBtOHO9tQkiVr97xcaonVoh4hBvkS2ScAldRgBZwuC+ICOkvcHEajCwDiFN9aAsNoTqBZygapBr3GXIZFOl3kSTrPuLg8oM5+LbS8NLjj1D6Fy3+p1JfszS4ZgLA1WUbYhkaDKAHZScwnZOMIBW8t1yDkq8F8Lb+nDAg2iARbOkoIHjaID1s6QhA0NoAa67CKgshcny4TcxNwWSRoayemWB2/aS1ubhPPkHAZ+Y6KV9MpmQfYogxixnMABc5yp69joZh9sRQvabnAHNixrF8uehoVjcDu7cNhV631rJtOH3qfrydxhTVnh7U2XY3NZgyyZvanb7KVD6d3lMX9Qt1FJcjCntImhoEl0vaenYPFAQBbCC0hsA80nKZvCh13ooOO8fzZYeidEqcq2iypZmfKSrQCQdTgmVIyJXSm/C9+PPTiA4e/2l8Xc6WyWjEBaRJh1ITP+8oo6B8B+gxkFXkLgsqvGXMiMShN2OInm+gULHchdHRyj32OhkEIrJEXbk9SRqMA5TrTdnNknoeRWtPrEXf27S4Q5zhYECojPElHmN1LC6JjklszWxK0I4BUNiGbBYCQr5fISslCVDbN5uaKlcUFtau+WxfNFjWqbda2m1KzAd1FUqJ/PhJJKdddmn+gt2KYpfolZkbHFeoLAZkY50k3MfakrBPpwX0Ec0/ohjNFFnPcV+aXdgoK1Cd1Qoech9k832VJa6qiwHTWPVwgGIyEd5M2C/IYEMcju496Jat1A1gdeHi0oc04DN+1Ma1nkLrMnkpC1ET1Xd5B875p5HU71HoPQ1jqlDFK5yPdJswAokOjeyVIzhn0kW6ctXCAAfERyIDNZU6pUXrWX5sXPcPTjUmeaWBgJI/9Bc03XU0a7RbAbq6f0vkYHaPkmeaIqtuk+1scabZisZVITv94kztvKfbihiRHHlASCdjaRHQwAFOCTgsN+6a2C4d6Kl+Ku+wwuxpP88dtsbmA0ks44vuVCit8i4vRizU/fwPpG6YjIXSMy9xc8qCpsS3/KBwNWJUpSYTNFftOiwYQ/LRHr7nqgjjRMH7z9uw0fkziUnpYoHFbMQYy4ZzADW9iDexybp8MOAgUutrX/8MdgbNNx7+pzzdAJG7/Hu4qnSrf+wEcIaVyTsnFRtQJ95P21Cpvi4xFd3RcxLY+CvteeEenQzR23Jb9dWyro0CPnR1RyByEHxAa6lIiqfsfrVlfUbOoPGFdk6i5aB8HslKROeFAdr32OlPUyeafZGSGLXoMZS7Gw/FkvHjASgat3HS739JUge3zSwRKLTE9zN0BrKbL7I/7i1QvxXuJz8A0U5fpVBf9bXkkoxEXz2P1/QuMDMYYyI5NM+KYOFy6VtOb4fjqbnaXs/9KbxRqf27OY+kAVcNPTbhAtinJxyO2njgbQplil4jmy176XVmORTZ1RaKCiF0e7uMfbhbDDuwohtWL8fSB+T2GTDIlIJBDNkpSMIXxubCw6xn8Rt8apRj9V1bGLMqvMQ1uZpJhNdf9CPF24j7hrR7YE++cXUjpdKZKm+0RTvu7VfTLRlmO+RCkK6u2zZvZ9GP2cAuvfqLn9ht+dgafVW6bqT3YmqQTHaNnXwxk82CL5nlk0xDhA9lzowGHbQZhHYd7at69qz95aNuXQqSHiMS4wu1PeQJS88IpYlAXpLW6WKgHJ6NHZkHSmxjJAtbeTRURKY1YsO5JOr/KT6LZECjeiZ48+fhZfoLMoV+FeDEfL5VibcWUlEFeMLXGhJPMIHNhZoaBoi4IRk8Z0+liMW9ZGnAGiTL2IDso7oTfftl4YDlAdt3aqcswrTGSY4NXkUBejWb++eqNTH0bV69Z/ZgNL1q2KsScrGlcvqBnnyCZ9U9GoQZ7WE1LYHnwSxtY1hXjSRtbzPpFaxZvKD9iLzoQN2Fy1cAl4KNc4Jpzu7C4yxBhNr+6OZ3FWdqC2aWgXuVAXuXgXeWAXRUgXTXAXGFloo1wqHfFUxUVvPLCqDwsC4I00QtYUQvqgJ7QnYw/iqy/xljNG1QNAi9H6IaYfAkxJ30OxLTbwhK36VR1q3o1109FbfLXSn6JSFxN7xKqMdakaBrcmGRX6cUD6n0uUgbBNMxlQWXTIbeFzAJJ5hvxml7F8DqMIbdNthcLvjfGb8h6ZX6blhCNWQ57PfPR9pOQR27tWCkYAlti9z0MKhJ7JVg+QuKrnerAS4RzAcIcQVEjxZQVPqCM4/u2LzRs74lMvD2RF33vfF4Up/Zjk6q6/RMpVtg3h/EwHf3hGFvD+IcfonQw9D/9LkqHF8Vr4VLvI6DkM8izRcq6Y1VvgzfpBNLb8UCsRaWY46K+Ky+jsI4KM1iwL4tO8Z9DUAUL0Fhutqv+/p4CHi5h3fwBkM6eQTlHX5UUVIfcJohW2HsYibPtiDgR89lBP/wpMMI1YbTtHYfGwYRu4NF9s1Visx9mc3DNG786PjSbgremJs7C3TGq7UYKOpr6D7YRFYE+U6ybYKk7lMI/nTwgWLVFX+fCjq0sqDox1jPeCTn22KoJljDBqEEeJhIkpr2KAajfgCMwZfZB9YxB8Ud/Unq5UCdSOobKujT07lxkBMK+C/wVS+lZu8H5WFkbYRcgdPpRy3F3vF6esPiROdgnJzMlNIBBQWBRqwTk0H7E6pxuGUaDCNVt+4hg6ypqKvCUBgUh6IPlET8ViByBojxZqVZVg1fEf5Ms41oSACbsRF5pniKqLs2rrjRR6x6BN+gFOtF41vsZRO7VW1tL1yQ+WC5L9fFBT86olJTdRGbnMThojJfY4HiCcSFzXxfBmGg4kC9thbmBhVRzePlcu1WlrTGXS1gJwesEe5Wl06us1W+XlL5qVhck15Aobi+pfMrA6datesIiwakezTQPi4Pa8cBLjdmXRhKwTFIpeUD4YDwIbbejVvVqm3dYFmp53es9Za9We/mTYxG1b2181psK2R5ztPd8q1PxqrTc04DJUiIaDJBH4ODu2tooyPlO58raOH37Yskiz2Ykshk/9Bol9JG9kYW+BSNcnbT6ykAoDuSRy7Y0ltmlZC1TtpM+ATRJRMaRunibEr7xRETYCeNTkVLt0BToo3E3Qr/lYt/hvs5IxWDWCSt8yjUZmySEJRK3cZwuhbi0jYAzm6qzByIiKGK6uSe7koiA1km/v+NJqjprz5yJE5RCcLpfp9NEzXdZh3QaAZVubhcPoaxV2DAjfgSUF1Oj58vxvWLwknK+Hv4xVonIVVa8+/EoP3rZf/USK3ZFwk8H8CjMfwf0S7teEPbwMTual0JkN2dOWKiyi/x2Dp+YR6YWtXO4Gt6NrrIFdxLMd+TseR9/7Zlzf2cPnEYrMGqKSRtrqgNk5piBXuFBguAr3zIGadITLKKR55alzCJd4E+fn+9/iP7PNEirlRdUApxc4Plmd9E/JSx9HbVIGDB4+Y3AA9+R3ktXGMAL+21DkThD4w8exv8bf7vII73Lpjg7za5SiBhqzJCm9VAlPOXRMACNmI1qEaPThVe2pvhNKgF3ts9i/aI60NI92CaDt7J2TcavTfe9apTv+xICx/hanJY0gqP7BJIIrsfTkS7QhVrJkN8QcUq3wlt7n2djEX0FLlq8iwwKO1VydeCcjsina5yfTRckhGdC7VxkzBPcMNrLsnVfm3EqB1y76/DD4eTH0whgYuY54MPERNBCk9hSRzJgcqlRvhR8yF6fN+FL1jNpVzv+Aj5ec+eSAlqHQ70VUTX5pgmAly4hregxAvkDUGHF6BFUhMBh7CYbjvEA4CLFonmbzrcdfOBTe8tNPy2FVS8PTRTT1JeNT6zbL9kk3NBQEushyDcsjISNu6LGTI3nMgTJrZ1Lzur6eziiizIZ764SYjgQ9Dgy38G3fEf+CVvb1PoqfwQFKKx1A24WtGawTekdYYhDLniWLkjOTcOzKJ+mt/n1bLmymDqfQVnvOrWtTdKb4SiNdN83mIqTrQ7kwfKulRLUCbjeMk1D6v2cjjSqXpw/TBUJ1Cm9aV46D6LmxWNA2JRfc7OOaq/DnlB67VJRs5ny2xA1OPbuiqg5vfuaihocjNYiagbQV53UsLHCCpJWqIDQr7i5Y4DfkDEgYxeTNM+j5L2eDahi9USZnDH7NGYoW0B3VpIWsb6mnH0iHhq6t7N80bUyAZIyVbMgmbC8SRKAWk0SkWsI4LciE+2J+B3S20/UhjgDKIYX6SSXLWYpW7X063l2dTdJ5+Z7QL316ngsijPnIXorQ78dt22Kquwm744C2rL5BusrwKZSP/yvOG16wHPzv+YpRQM9OUlZ6ooGFxIosZq1mIaFnVa6R7GbMqmMIFPbPtjFJ1BWoEYazibqY3hJu6wFjq+divsO0IltPe9wDXBbpc9HbruPhqZv63k0VVM0fS73fFvHo5n/2t2cV5BeS4h8E90xZw0k0KLmpiEX3iIoh9A2bk2M0JG5al4EVdlzxwQFXO77W8DVAmVmTGzzXO14uJ7P7q6u8aUwr1AZk9k9GZXQ36dUnaESw7c/KWrUju1U5Ow9IVWTIAUG1PHU+0o2SzK/u6+jDq7KQEgs/eMQUWKXI6fTRYEROq2DKVAqA97j7wU1nhQ2W2/CaszXoEurH21cReCExq4X0yuyTcLrg/qSOjwSNHEf/z+U4gCPV6NOsoJEuE94wMstn+RjvK5VRhqqbA27GgiKYno5vrqbe4LJx9m5F34WCXjUczV3Xiy4XB2haQD1A0PVIf0UpL0FSnsA2y10wfIr1DqVt5JWMzeHilOqB2CFNEDdUByCYxdlqe3eQZ6CCSbAxBEJbdq0limeLtMMzZGF0EI3gUgga4dpQlnv/JZd7OdAb84508YqW8pA16twEFSr2MzQcddIxPFN2fgrWNo8kLX9eV5rxDZqYGo3A9XCqoya5QaUwEvKG9bJFeKlk4H38CDZPzpOnn3/HboK23KwcpfZcitGD6jz9lZaMoU9c9nFcjPGvaJLL0HownBKgb+NXsXsrXWsF963AzoN4nn7y6wqM/P/X0flbpGwqmb9qHU1tvdYg2ck9c+3bC9y+5DZnHvG5eMRNJ8wDpMxWFbqvL4eSdqeZg60LIltLtnfRx7r+hAuLaX/BhoPnvHA3gAA

Copy link
Copy Markdown
Contributor Author

@opencode-agent Apply and verify the exact patch payload in comment #issuecomment-5205775526 now, but only if branch HEAD is still fb4ccefae3899c617bf969a3c93be3924177b23c. The payload/digest/blob/path contract in that comment supersedes free-form implementation. Commit exactly the four verified files, keep the PR draft, and stop without writing on any mismatch. Do not merge, rebase, retarget, release, publish, or modify any other path.

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

Please review exact current head eae6c374dcf58237c6c6e7cd9c12409c7d1425c3. Keep the PR draft; do not infer success from the draft status check. Focus on bounded pre-materialization ZIP central-directory parsing, streaming gzip/tar physical-header limits, sequential tar semantics, stable fail-closed errors, canonical wheel/sdist compatibility, and regression completeness.

@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown
🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

expected_head='eae6c374dcf58237c6c6e7cd9c12409c7d1425c3'
base='224e6d253a7957e981515ec66260fb1c2cf84a31'

printf '%s\n' '== HEAD =='
git rev-parse HEAD
git status --short
printf '%s\n' '== changed paths =='
git diff --name-status "$base"...HEAD
printf '%s\n' '== diff stat =='
git diff --stat "$base"...HEAD

printf '%s\n' '== module outline =='
ast-grep outline scripts/ci/generate_release_sbom.py --items all --type function --view expanded
printf '%s\n' '== boundary test outline =='
ast-grep outline tests/test_release_sbom_member_enumeration_bound.py --items all --type function --view expanded

printf '%s\n' '== materialization and parser call sites =='
rg -n -C 4 -P '\b(ZipFile|infolist|getmembers|tarfile\.open|open_archive|r:gz|r\|gz)\b' \
  scripts/ci/generate_release_sbom.py tests/test_release_sbom_member_enumeration_bound.py

printf '%s\n' '== target diff =='
git diff --find-renames --find-copies --unified=50 "$base"...HEAD -- \
  scripts/ci/generate_release_sbom.py \
  tests/test_release_sbom_member_enumeration_bound.py \
  docs/sbom-release-evidence.md \
  CHANGELOG.md

Length of output: 90303


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '== bounded descriptor reader and artifact flow =='
nl -ba scripts/ci/generate_release_sbom.py | sed -n '55,175p'
printf '%s\n' '== ZIP preflight implementation =='
nl -ba scripts/ci/generate_release_sbom.py | sed -n '215,355p'
printf '%s\n' '== gzip/tar preflight and streaming semantics =='
nl -ba scripts/ci/generate_release_sbom.py | sed -n '345,535p'

printf '%s\n' '== helper call sites and potentially unbounded reads =='
rg -n -C 3 -P '\b(_preflight_wheel_members|_preflight_sdist_members|_read_expanded|\.read\s*\(\s*\)|\.read\s*\(\s*-1\s*\)|getmembers\s*\()' \
  scripts/ci/generate_release_sbom.py tests --glob '*.py'

printf '%s\n' '== test and coverage configuration =='
fd -HI -t f '^(pyproject\.toml|pytest\.ini|setup\.cfg|tox\.ini|\.coveragerc)$' . \
  -x sh -c 'echo "---- $1"; cat "$1"' sh {}

printf '%s\n' '== existing generator test references =='
rg -n -C 2 -P '\b(build_sbom|_wheel_metadata|_sdist_metadata|generate_release_sbom)\b' \
  tests --glob '*.py'

printf '%s\n' '== changed-line coverage markers, if present =='
rg -n -C 2 -P '(pragma:\s*no cover|coverage|branch)' \
  scripts/ci/generate_release_sbom.py tests/test_release_sbom_member_enumeration_bound.py

Length of output: 1630


🏁 Script executed:

#!/bin/bash
set -euo pipefail

show_range() {
  file="$1"
  start="$2"
  end="$3"
  awk -v start="$start" -v end="$end" \
    'NR >= start && NR <= end { printf "%6d  %s\n", NR, $0 }' "$file"
}

printf '%s\n' '== bounded descriptor reader and artifact flow =='
show_range scripts/ci/generate_release_sbom.py 55 175
printf '%s\n' '== ZIP preflight implementation =='
show_range scripts/ci/generate_release_sbom.py 215 355
printf '%s\n' '== gzip/tar preflight and streaming semantics =='
show_range scripts/ci/generate_release_sbom.py 345 535

printf '%s\n' '== helper call sites and potentially unbounded reads =='
rg -n -C 3 -P '\b(_preflight_wheel_members|_preflight_sdist_members|_read_expanded|\.read\s*\(\s*\)|\.read\s*\(\s*-1\s*\)|getmembers\s*\()' \
  scripts/ci/generate_release_sbom.py tests --glob '*.py'

printf '%s\n' '== test and coverage configuration =='
while IFS= read -r file; do
  printf '%s\n' "---- $file"
  cat "$file"
done < <(fd -HI -t f '^(pyproject\.toml|pytest\.ini|setup\.cfg|tox\.ini|\.coveragerc)$' .)

printf '%s\n' '== existing generator test references =='
rg -n -C 2 -P '\b(build_sbom|_wheel_metadata|_sdist_metadata|generate_release_sbom)\b' \
  tests --glob '*.py'

printf '%s\n' '== changed-line coverage markers, if present =='
rg -n -C 2 -P '(pragma:\s*no cover|coverage|branch)' \
  scripts/ci/generate_release_sbom.py tests/test_release_sbom_member_enumeration_bound.py || true

Length of output: 44381


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '== repository test and coverage configuration files =='
find . -type f \( \
  -name pyproject.toml -o -name pytest.ini -o -name setup.cfg -o \
  -name tox.ini -o -name .coveragerc \
\) -print | sort | while IFS= read -r file; do
  printf '%s\n' "---- $file"
  cat "$file"
done

printf '%s\n' '== existing bounded-reader regression coverage =='
awk 'NR >= 250 && NR <= 380 { printf "%6d  %s\n", NR, $0 }' \
  tests/test_release_sbom_archive_bound.py

printf '%s\n' '== all release-SBOM test modules =='
find tests -maxdepth 1 -type f -name '*release_sbom*.py' -print | sort

printf '%s\n' '== test assertions for new public errors and parser ordering =='
rg -n -C 2 -P \
  '(ZipFile materialized|tarfile\.open|archive-member safety bound|expanded-tar safety bound|extension header exceeds|not a valid (ZIP|gzip tar))' \
  tests/test_release_sbom_member_enumeration_bound.py tests/test_release_sbom_archive_bound.py

printf '%s\n' '== diff whitespace and exact changed-path check =='
git diff --check 224e6d253a7957e981515ec66260fb1c2cf84a31...HEAD
git diff --name-only 224e6d253a7957e981515ec66260fb1c2cf84a31...HEAD | sort

Length of output: 14937


@seonghobae I completed a static review of exact head eae6c374dcf58237c6c6e7cd9c12409c7d1425c3.

I found one issue.

  • pyproject.toml configures coverage with source = ["egressweave"]. This excludes scripts/ci/generate_release_sbom.py. The new tests execute the generator, but the configured 100% statement and branch coverage gate cannot measure its new production code. Add a coverage invocation or configuration that includes scripts/ci/generate_release_sbom.py, and preserve the 100% requirement for that file.

The static implementation review found that:

  • ZIP central-directory preflight occurs before zipfile.ZipFile.
  • ZIP preflight uses bounded descriptor reads and validates EOCD and central-directory agreement.
  • gzip/tar preflight occurs before sequential tarfile.open(..., mode="r|gz").
  • Tar processing does not call TarFile.getmembers().
  • The four-file allowlist is preserved.
  • The documentation and changelog changes match the stated resource boundaries.
  • I did not execute tests or CI in this sandbox. Hosted checks remain required.
⚠️ Action not completed

Review rate limited.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown
Contributor Author

@opencode-agent Apply one bounded changelog-only completion to exact current head 52f887efea582e4e75c3649f3e441f1cb0055819 on branch security/bound-archive-member-enumeration. Stop without writing if the PR head differs.

Modify only CHANGELOG.md.

Under [Unreleased]Security, add one concise entry stating that direct release-SBOM archive handling now counts ZIP central-directory records before ZipFile materialization, preflights gzip/tar physical headers before semantic parsing, caps archives at 10,000 physical members, caps aggregate expanded tar processing at 512 MiB, caps each PAX/GNU extension payload at 1 MiB, uses sequential tar parsing without getmembers(), and fails closed on unnecessary ZIP64/multi-disk, sparse, link/device/FIFO, malformed, truncated, or unsupported special forms. Keep the wording clear that canonical wheel and ordinary gzip sdist inputs remain supported and that no provenance, signing, publication, attestation, or SLSA claim follows.

Do not change code, tests, docs, .github, workflows, dependencies, credentials, permissions, refs, release behavior, version, or any other path. Commit exactly CHANGELOG.md, keep the PR draft, and do not merge, rebase, retarget, mark ready, enable auto-merge, release, or publish. After the commit, report the new exact head and leave all exact-head CI/security/review gates pending until they actually complete.

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

Retry the exact-head review for eae6c374dcf58237c6c6e7cd9c12409c7d1425c3 now that the temporary review window has reopened. Review all four changed files; keep the PR draft.

@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown

@seonghobae I will retry the review for exact head eae6c374dcf58237c6c6e7cd9c12409c7d1425c3. I will review all four changed files. The PR will remain a draft.

✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
tests/test_release_sbom_member_enumeration_bound.py (1)

159-165: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

MAX_ARCHIVE_MEMBERS도 이 상수 고정 테스트에 포함하십시오.

test_new_archive_bounds_are_exactMAX_EXPANDED_TAR_BYTES, MAX_TAR_EXTENSION_BYTES, ZIP64_EOCD_LOCATOR_SIZE만 확인합니다. EXPECTED_MAX_ARCHIVE_MEMBERS는 fixture 크기 계산에만 사용됩니다. 생성기에서 MAX_ARCHIVE_MEMBERS가 변경되면 fixture 멤버 수만 어긋나고 이 테스트는 계속 통과합니다. 두 값의 일치를 명시적으로 고정하십시오.

♻️ 제안 수정
     assert generator.MAX_EXPANDED_TAR_BYTES == 512 * 1024 * 1024
     assert generator.MAX_TAR_EXTENSION_BYTES == 1 * 1024 * 1024
     assert generator.ZIP64_EOCD_LOCATOR_SIZE == 20
+    assert generator.MAX_ARCHIVE_MEMBERS == EXPECTED_MAX_ARCHIVE_MEMBERS
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_release_sbom_member_enumeration_bound.py` around lines 159 - 165,
Update test_new_archive_bounds_are_exact to also assert
generator.MAX_ARCHIVE_MEMBERS equals EXPECTED_MAX_ARCHIVE_MEMBERS, explicitly
locking the generator limit to the fixture’s expected member count while
preserving the existing constant checks.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@scripts/ci/generate_release_sbom.py`:
- Around line 434-437: Update the exception handling around gzip.GzipFile.read()
to import zlib and include zlib.error alongside OSError and EOFError, while
preserving the existing SystemExit passthrough and normalization to
SystemExit(invalid).

---

Nitpick comments:
In `@tests/test_release_sbom_member_enumeration_bound.py`:
- Around line 159-165: Update test_new_archive_bounds_are_exact to also assert
generator.MAX_ARCHIVE_MEMBERS equals EXPECTED_MAX_ARCHIVE_MEMBERS, explicitly
locking the generator limit to the fixture’s expected member count while
preserving the existing constant checks.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 00098112-9592-47d4-95b0-ca7520d3f0d1

📥 Commits

Reviewing files that changed from the base of the PR and between 224e6d2 and eae6c37.

📒 Files selected for processing (4)
  • CHANGELOG.md
  • docs/sbom-release-evidence.md
  • scripts/ci/generate_release_sbom.py
  • tests/test_release_sbom_member_enumeration_bound.py

Comment thread scripts/ci/generate_release_sbom.py

Copy link
Copy Markdown
Contributor Author

@opencode-agent Apply the exact-head review-test repair on branch security/bound-archive-member-enumeration only if the current head is exactly 99c06b90772aec91fc27c95ffcc7745bb163dde6.

Modify only tests/test_release_sbom_member_enumeration_bound.py:

  1. In test_new_archive_bounds_are_exact, add assert generator.MAX_ARCHIVE_MEMBERS == EXPECTED_MAX_ARCHIVE_MEMBERS after the existing constant assertions.
  2. In test_tar_preflight_normalizes_gzip_errors_and_rewinds, after the existing invalid-gzip case, add a damaged-deflate regression: create damaged-deflate.tar.gz from bytearray(gzip.compress(b"\x00" * 4096, mtime=0)), replace bytes [15:60] with b"\xff" * 45, write it, call _preflight_sdist_members, require SystemExit matching not a valid gzip tar, and assert the stream rewinds to position 0.

Run the focused test module. Do not modify any other path, workflow, dependency, credential, permission, ref, base, PR state, release behavior, or publication behavior. Do not rebase, retarget, mark ready, merge, or enable auto-merge. Stop without writing if the exact head differs.

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

Review exact current head 24fbe1f9acb0489e9c628db23f9fe3612ef3112a. The prior zlib.error finding is addressed and its thread resolved; the archive-member constant is now explicitly locked to the overmember fixtures. Keep the PR draft. Review all four changed files and do not infer success from queued checks.

@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

expected_head='24fbe1f9acb0489e9c628db23f9fe3612ef3112a'
base='224e6d253a7957e981515ec66260fb1c2cf84a31'

actual_head="$(git rev-parse HEAD)"
printf 'HEAD=%s\n' "$actual_head"
if [ "$actual_head" != "$expected_head" ]; then
  printf 'ERROR: expected exact head %s\n' "$expected_head" >&2
  exit 2
fi

printf '\n== Changed paths ==\n'
git diff --name-status "$base...$expected_head"
printf '\n== Diff check ==\n'
git diff --check "$base...$expected_head"

printf '\n== Targeted implementation outline ==\n'
ast-grep outline scripts/ci/generate_release_sbom.py --items all --type function --view expanded

printf '\n== Targeted test outline ==\n'
ast-grep outline tests/test_release_sbom_member_enumeration_bound.py --items all --type function --view expanded

printf '\n== Parser, decompression, and bounded-read call sites ==\n'
rg -n -C 5 -P '\b(ZipFile|infolist|getmembers|tarfile\.open|r:gz|r\|gz|zlib\.error|gzip\.(BadGzipFile|GzipFile)|_preflight_(wheel|sdist)_members|MAX_ARCHIVE_MEMBERS|MAX_EXPANDED_TAR_BYTES|MAX_TAR_EXTENSION_BYTES|_read_expanded)\b' \
  scripts/ci/generate_release_sbom.py \
  tests/test_release_sbom_member_enumeration_bound.py

printf '\n== Implementation: archive preflight and semantic parser ==\n'
nl -ba scripts/ci/generate_release_sbom.py | sed -n '1,620p'

printf '\n== Test: repaired regression and neighboring checks ==\n'
rg -n -C 18 -P 'def test_(new_archive_bounds_are_exact|tar_preflight_normalizes_gzip_errors_and_rewinds|wheel_member_bound_precedes_zipfile_materialization|sdist_member_bound_does_not_materialize_getmembers|canonical_wheel_and_sdist_remain_compatible)' \
  tests/test_release_sbom_member_enumeration_bound.py

printf '\n== Four-file diff ==\n'
git diff --find-renames --find-copies --unified=35 "$base...$expected_head" -- \
  CHANGELOG.md \
  docs/sbom-release-evidence.md \
  scripts/ci/generate_release_sbom.py \
  tests/test_release_sbom_member_enumeration_bound.py

printf '\n== Coverage configuration ==\n'
fd -HI -t f '^(pyproject\.toml|pytest\.ini|setup\.cfg|tox\.ini|\.coveragerc)$' . \
  -x sh -c 'printf "\\n---- %s ----\\n" "$1"; cat "$1"' sh {}

Length of output: 42455


🏁 Script executed:

#!/bin/bash
set -euo pipefail

expected_head='24fbe1f9acb0489e9c628db23f9fe3612ef3112a'
base='224e6d253a7957e981515ec66260fb1c2cf84a31'
test "$(git rev-parse HEAD)" = "$expected_head"

show_range() {
  file="$1"
  start="$2"
  end="$3"
  printf '\n== %s, lines %s-%s ==\n' "$file" "$start" "$end"
  awk -v start="$start" -v end="$end" \
    'NR >= start && NR <= end { printf "%6d  %s\n", NR, $0 }' "$file"
}

show_range scripts/ci/generate_release_sbom.py 55 175
show_range scripts/ci/generate_release_sbom.py 219 445
show_range scripts/ci/generate_release_sbom.py 443 540
show_range tests/test_release_sbom_member_enumeration_bound.py 1 390

printf '\n== Semantic tar parser exception handling and unbounded APIs ==\n'
rg -n -C 8 -P \
  'except\s+\(|except\s+.*zlib\.error|tarfile\.open|for\s+member\s+in\s+archive|extractfile|\.read\s*\(\s*\)|getmembers\s*\(' \
  scripts/ci/generate_release_sbom.py

printf '\n== Documentation and changelog diff ==\n'
git diff --unified=30 "$base...$expected_head" -- CHANGELOG.md docs/sbom-release-evidence.md

printf '\n== Production and test diff ==\n'
git diff --unified=20 "$base...$expected_head" -- \
  scripts/ci/generate_release_sbom.py \
  tests/test_release_sbom_member_enumeration_bound.py

printf '\n== Coverage configuration ==\n'
for file in $(find . -type f \( -name pyproject.toml -o -name pytest.ini -o -name setup.cfg -o -name tox.ini -o -name .coveragerc \) -print | sort); do
  printf '\n---- %s ----\n' "$file"
  cat "$file"
done

printf '\n== Historical coverage-related changes in this PR ==\n'
git diff --name-status "$base...$expected_head" -- \
  pyproject.toml pytest.ini setup.cfg tox.ini .coveragerc

Length of output: 104385


@seonghobae I completed a static review of exact head 24fbe1f9acb0489e9c628db23f9fe3612ef3112a.

I found one issue.

  • pyproject.toml sets coverage source = ["egressweave"]. This excludes scripts/ci/generate_release_sbom.py. Therefore, the configured 100% statement and branch coverage gate does not measure the new archive parsing code or its regressions. Add a coverage invocation or configuration that includes this standalone generator module and preserves the 100% requirement.

I confirmed the stated repairs:

  • _preflight_sdist_members() now normalizes zlib.error to release source distribution is not a valid gzip tar and rewinds the stream.
  • The damaged-deflate regression verifies this behavior.
  • MAX_ARCHIVE_MEMBERS is explicitly locked to the overmember fixture value.
  • ZIP central-directory preflight runs before zipfile.ZipFile.
  • Tar preflight runs before sequential tarfile.open(..., mode="r|gz").
  • The semantic tar parser does not call TarFile.getmembers().
  • The four-file change scope, changelog, and operator documentation are consistent with the implemented boundaries.

I did not execute tests, CI, lint, packaging, or security checks. These gates remain pending.

You are interacting with an AI system.

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

Review exact current head e2c3db6850c055f6ae5f904e00b1ca389a788cd1. The prior deflate-normalization and archive-limit findings are fixed. The coverage boundary now uses source_dirs = ["src/egressweave", "scripts/ci"], so every Python release-control script is subject to the same 100% statement and branch gate. Keep the PR draft and review all five changed files; queued CI is not success.

@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

expected_head='e2c3db6850c055f6ae5f904e00b1ca389a788cd1'
base='224e6d253a7957e981515ec66260fb1c2cf84a31'

actual_head="$(git rev-parse HEAD)"
printf 'HEAD=%s\n' "$actual_head"
if [ "$actual_head" != "$expected_head" ]; then
  printf 'ERROR: expected exact head %s\n' "$expected_head" >&2
  exit 2
fi

printf '\n== Changed paths ==\n'
git diff --name-status "$base...$expected_head"
printf '\n== Changed path count ==\n'
git diff --name-only "$base...$expected_head" | wc -l
printf '\n== Diff check ==\n'
git diff --check "$base...$expected_head"

printf '\n== Coverage configuration ==\n'
fd -HI -t f '^(pyproject\.toml|pytest\.ini|setup\.cfg|tox\.ini|\.coveragerc)$' . \
  -x sh -c 'printf "\n---- %s ----\n" "$1"; cat "$1"' sh {}

printf '\n== Archive generator outline ==\n'
ast-grep outline scripts/ci/generate_release_sbom.py --items all --type function --view expanded

printf '\n== Archive boundary test outline ==\n'
ast-grep outline tests/test_release_sbom_member_enumeration_bound.py --items all --type function --view expanded

printf '\n== Archive parser and coverage call sites ==\n'
rg -n -C 5 -P '\b(ZipFile|infolist|getmembers|tarfile\.open|r:gz|r\|gz|zlib\.error|gzip\.(BadGzipFile|GzipFile)|_preflight_(wheel|sdist)_members|MAX_ARCHIVE_MEMBERS|MAX_EXPANDED_TAR_BYTES|MAX_TAR_EXTENSION_BYTES|source_dirs|source\s*=|coverage)\b' \
  pyproject.toml scripts/ci/generate_release_sbom.py tests/test_release_sbom_member_enumeration_bound.py

printf '\n== Five-file diff ==\n'
git diff --find-renames --find-copies --unified=35 "$base...$expected_head" -- \
  CHANGELOG.md \
  docs/sbom-release-evidence.md \
  pyproject.toml \
  scripts/ci/generate_release_sbom.py \
  tests/test_release_sbom_member_enumeration_bound.py

Length of output: 104345


🏁 Script executed:

#!/bin/bash
set -euo pipefail

expected_head='e2c3db6850c055f6ae5f904e00b1ca389a788cd1'
test "$(git rev-parse HEAD)" = "$expected_head"

show_range() {
  file="$1"
  start="$2"
  end="$3"
  printf '\n== %s, lines %s-%s ==\n' "$file" "$start" "$end"
  awk -v start="$start" -v end="$end" \
    'NR >= start && NR <= end { printf "%6d  %s\n", NR, $0 }' "$file"
}

show_range scripts/ci/generate_release_sbom.py 378 540
show_range scripts/ci/generate_release_sbom.py 750 845
show_range tests/test_release_sbom_member_enumeration_bound.py 271 368

printf '\n== Python files under scripts/ci ==\n'
fd -t f -e py . scripts/ci | sort

printf '\n== Coverage commands and source-directory references ==\n'
rg -n -C 4 -P '(coverage(\s+run|\s+report|\s+combine)?|pytest|source_dirs|generate_release_sbom)' \
  .github scripts README.md docs pyproject.toml \
  -g '*.yml' -g '*.yaml' -g '*.md' -g '*.toml' -g '*.py' 2>/dev/null || true

printf '\n== Tests that load or execute each scripts/ci Python module ==\n'
for file in $(fd -t f -e py . scripts/ci | sort); do
  name="$(basename "$file" .py)"
  printf '\n---- %s ----\n' "$file"
  rg -n -C 2 -P "(${name//./\\.}|${file//\//\\/})" tests --glob '*.py' || true
done

printf '\n== Tar semantic exception paths ==\n'
rg -n -C 8 -P 'def _sdist_metadata|except tarfile\.TarError|except \(.*zlib\.error|tarfile\.open|extractfile|selected_payload|_sha256_file|_artifact_metadata|build_sbom' \
  scripts/ci/generate_release_sbom.py

printf '\n== Changed coverage configuration validation context ==\n'
git show "$expected_head:pyproject.toml" | sed -n '65,82p'

Length of output: 55378


🏁 Script executed:

#!/bin/bash
set -euo pipefail

expected_head='e2c3db6850c055f6ae5f904e00b1ca389a788cd1'
test "$(git rev-parse HEAD)" = "$expected_head"

printf '%s\n' '== Hourly guard outline =='
ast-grep outline scripts/ci/hourly_product_guard.py --items all --view expanded

printf '%s\n' '== Hourly guard executable entry points =='
rg -n -C 5 -P '^(def |class |if __name__|[[:space:]]*raise SystemExit|[[:space:]]*main\()' \
  scripts/ci/hourly_product_guard.py

printf '%s\n' '== References to hourly product guard in tests and coverage startup =='
rg -n -C 3 -P '(hourly_product_guard|COVERAGE_PROCESS_START|coverage\.process_startup|coverage run|coverage report)' \
  tests .github pyproject.toml \
  -g '*.py' -g '*.yml' -g '*.yaml' -g '*.toml' || true

printf '%s\n' '== Coverage configuration files =='
fd -HI -t f '^(pyproject\.toml|\.coveragerc|setup\.cfg|tox\.ini|sitecustomize\.py)$' . \
  -x sh -c 'printf "\n---- %s ----\n" "$1"; cat "$1"' sh {}

Length of output: 14031


@seonghobae I reviewed exact head e2c3db6850c055f6ae5f904e00b1ca389a788cd1.

I found one issue.

  • pyproject.toml Line 74 adds all scripts/ci modules to coverage. scripts/ci/hourly_product_guard.py has executable production code, but no test imports it. Both .github/workflows/ci.yml and .github/workflows/release.yml run coverage report -m before python scripts/ci/hourly_product_guard.py self-test. Coverage will report this module as unexecuted and fail the required 100% gate. Add coverage-backed tests or coverage invocations for this module before coverage report, or limit source_dirs to the intended release-control modules.

I confirmed the earlier repairs:

  • The archive-member constant is locked to 10_000.
  • Gzip preflight normalizes zlib.error and rewinds the descriptor.
  • ZIP preflight runs before zipfile.ZipFile.
  • Tar preflight runs before sequential tarfile.open(..., mode="r|gz").
  • The semantic tar parser does not call TarFile.getmembers().
  • The changed-file scope contains the requested five files.

I did not execute tests, CI, lint, package acceptance, or security checks. These checks remain pending.

You are interacting with an AI system.

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@opencode-agent Perform a read-only review of exact current head b8526736db3bb16ed88290ca64fd07cf54232805 for PR #70. Do not modify any file, branch, ref, workflow, or PR state.

Treat all checks and reviews on e2c3db6850c055f6ae5f904e00b1ca389a788cd1 and earlier heads as prior-head evidence only. Verify that the two reproducible repository-contract failures from CI run 31121580673 are correctly repaired: the 100% statement-and-branch coverage contract now includes src/egressweave and scripts/ci, and the protected-main or organization-level reusable-workflow integration boundary remains explicit. Reassess the full archive resource-bound implementation, including pre-ZipFile central-directory enumeration, streaming gzip/tar physical validation, member/expanded-byte/extension-header limits, malformed and special-form rejection, sequential metadata extraction, zlib.error normalization, descriptor/digest controls, deterministic output, documentation, APA 7 references, and CHANGELOG accuracy.

The recorded stacked base remains 224e6d253a7957e981515ec66260fb1c2cf84a31, while current PR #68 head is 127e4577ea31ca20e7260a482343c892efeed6f9; GitHub reports this draft PR as conflicting, and no workflow run is currently associated with the exact current head. Do not approve, merge, mark ready, enable auto-merge, rebase, retarget, publish, release, or infer success from missing or prior-head checks. Anchor every finding to exact head b8526736db3bb16ed88290ca64fd07cf54232805.

@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

expected_head='b8526736db3bb16ed88290ca64fd07cf54232805'
base='224e6d253a7957e981515ec66260fb1c2cf84a31'

actual_head="$(git rev-parse HEAD)"
printf 'HEAD=%s\n' "$actual_head"
if [ "$actual_head" != "$expected_head" ]; then
  printf 'ERROR: expected exact head %s\n' "$expected_head" >&2
  exit 2
fi

printf '\n== Status and changed paths from recorded base ==\n'
git status --short
git diff --name-status "$base...$expected_head"
printf '\n== Whitespace check ==\n'
git diff --check "$base...$expected_head"

printf '\n== Configuration and workflow files ==\n'
fd -HI -t f '^(pyproject\.toml|.*\.(yml|yaml))$' . .github 2>/dev/null | sort

printf '\n== Coverage configuration ==\n'
sed -n '1,180p' pyproject.toml

printf '\n== Archive generator outline ==\n'
ast-grep outline scripts/ci/generate_release_sbom.py --items all --type function --view expanded

printf '\n== Archive boundary test outline ==\n'
ast-grep outline tests/test_release_sbom_member_enumeration_bound.py --items all --type function --view expanded

printf '\n== Archive and descriptor control call sites ==\n'
rg -n -C 5 -P '\b(ZipFile|infolist|getmembers|tarfile\.open|r:gz|r\|gz|zlib\.error|_preflight_(wheel|sdist)_members|MAX_ARCHIVE_MEMBERS|MAX_EXPANDED_TAR_BYTES|MAX_TAR_EXTENSION_BYTES|_read_expanded|sha256|stat|open)\b' \
  scripts/ci/generate_release_sbom.py \
  tests/test_release_sbom_member_enumeration_bound.py

printf '\n== CI workflow integration and coverage command call sites ==\n'
rg -n -C 6 -P '(workflow_call|uses:.*\.github/workflows|hourly_product_guard|coverage(\s+run|\s+report|\s+combine)?|source_dirs|protected|main)' \
  .github pyproject.toml \
  -g '*.yml' -g '*.yaml' -g '*.toml'

printf '\n== Five-file implementation diff ==\n'
git diff --find-renames --find-copies --unified=35 "$base...$expected_head" -- \
  CHANGELOG.md \
  docs/sbom-release-evidence.md \
  pyproject.toml \
  scripts/ci/generate_release_sbom.py \
  tests/test_release_sbom_member_enumeration_bound.py

printf '\n== Pull request and referenced PR metadata ==\n'
gh pr view 70 --json number,state,isDraft,headRefOid,baseRefOid,mergeStateStatus,statusCheckRollup,url
gh pr view 68 --json number,state,isDraft,headRefOid,baseRefOid,mergeStateStatus,url

Length of output: 148137


🏁 Script executed:

#!/bin/bash
set -euo pipefail

expected_head='b8526736db3bb16ed88290ca64fd07cf54232805'
base='224e6d253a7957e981515ec66260fb1c2cf84a31'
test "$(git rev-parse HEAD)" = "$expected_head"

show_range() {
  file="$1"
  start="$2"
  end="$3"
  printf '\n== %s, lines %s-%s ==\n' "$file" "$start" "$end"
  awk -v start="$start" -v end="$end" \
    'NR >= start && NR <= end { printf "%6d  %s\n", NR, $0 }' "$file"
}

printf '== Diff for the coverage contract test ==\n'
git diff --unified=50 "$base...$expected_head" -- tests/test_quality_contracts.py

printf '\n== scripts/ci Python modules ==\n'
fd -t f -e py . scripts/ci | sort

printf '\n== Tests that load, import, or execute each scripts/ci module ==\n'
for file in $(fd -t f -e py . scripts/ci | sort); do
  module="$(basename "$file" .py)"
  printf '\n---- %s ----\n' "$file"
  rg -n -C 3 -P "(${module}|${file//\//\\/})" tests --glob '*.py' || true
done

show_range scripts/ci/generate_release_sbom.py 55 175
show_range scripts/ci/generate_release_sbom.py 219 445
show_range scripts/ci/generate_release_sbom.py 443 540
show_range scripts/ci/generate_release_sbom.py 750 890

printf '\n== Archive API calls and exception handlers ==\n'
rg -n -C 7 -P \
  '(except |ZipFile|infolist|getmembers|tarfile\.open|extractfile|\.read\s*\(\s*\)|_preflight_|_sha256_file|_require_live_artifact_descriptor)' \
  scripts/ci/generate_release_sbom.py

printf '\n== Quality contract test source ==\n'
cat -n tests/test_quality_contracts.py

printf '\n== Documentation integration, archive controls, and references ==\n'
rg -n -C 3 -P \
  '(protected-main|organization-level reusable|resource bound|ZipFile|ZIP64|multi-disk|512 MiB|1 MiB|getmembers|CWE-409|CWE-770|Python 3\.13)' \
  docs/sbom-release-evidence.md CHANGELOG.md

printf '\n== Current PR checks, workflow runs, and mergeability metadata ==\n'
gh pr view 70 --json headRefOid,isDraft,mergeStateStatus,statusCheckRollup,url
gh run list --branch security/bound-archive-member-enumeration --limit 20 \
  --json databaseId,headSha,name,status,conclusion,event,url,createdAt

Length of output: 88084


@seonghobae I completed a read-only review of exact head b8526736db3bb16ed88290ca64fd07cf54232805.

I found one issue.

  • pyproject.toml Line 74 includes all of scripts/ci in the 100% statement-and-branch coverage scope. scripts/ci/hourly_product_guard.py is in that directory, but no test loads or executes it. .github/workflows/ci.yml runs coverage report -m before python scripts/ci/hourly_product_guard.py self-test. The same order exists in .github/workflows/release.yml. Therefore, the coverage report evaluates the unexecuted guard module and cannot meet the required 100% threshold. Add coverage-backed tests or run the guard under coverage before coverage report -m. Alternatively, scope source_dirs to only the intended scripts.

I confirmed these items by static inspection:

  • The coverage configuration includes src/egressweave and scripts/ci.
  • docs/sbom-release-evidence.md keeps the protected-main or organization-level reusable-workflow boundary explicit.
  • ZIP central-directory preflight runs before zipfile.ZipFile.
  • ZIP preflight uses bounded reads and rejects ZIP64, multi-disk, inconsistent EOCD data, malformed central records, and malformed extra fields.
  • gzip/tar physical preflight precedes tarfile.open(..., mode="r|gz").
  • The tar flow enforces the 10,000-member, 512 MiB expanded-data, and 1 MiB extension-header limits.
  • The semantic tar parser does not call TarFile.getmembers().
  • The gzip preflight normalizes zlib.error and rewinds the descriptor.
  • Descriptor identity checks, bounded parser reads, digest bracketing, and deterministic output remain present.
  • The documentation includes the requested resource-boundary guidance and APA 7 references for CWE-409, CWE-770, and Python 3.13 tarfile and zipfile.
  • The changelog entry accurately states the new archive controls.

The diff from the recorded base changes six files, not five. The added file is tests/test_quality_contracts.py.

I did not execute repository code, tests, CI, packaging, lint, or security scans. No ci workflow run is associated with this exact head. All earlier CI results are prior-head evidence only. PR #70 remains a draft.

You are interacting with an AI system.

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

Review only exact current head 56c9029588b67f329c334b4e56e1361087f8f651 against exact stacked base 224e6d253a7957e981515ec66260fb1c2cf84a31. The prior exact head b8526736db3bb16ed88290ca64fd07cf54232805 is superseded: its Python 3.10–3.13 lanes failed only because the protected-integration contract test required the literal phrase protected-main or organization-level reusable workflow, while the documentation line-wrapped between reusable and workflow. Current head changes only that documentation wrapping and preserves the previously reviewed archive-enumeration implementation, tests, standards references, and CHANGELOG.

Recheck the complete finite-resource archive boundary, including bounded wheel central-directory preflight before ZipFile table creation, sequential gzip/tar physical and semantic processing, 10,000-member and expanded-byte limits, bounded extension metadata, stable errors, zlib normalization, deterministic SBOM behavior, protected integration wording, and absence of workflow/credential/release changes. Do not approve, merge, mark ready, publish, or infer exact-head CI success from the previous head.

@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown
🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

expected_head='56c9029588b67f329c334b4e56e1361087f8f651'
base='224e6d253a7957e981515ec66260fb1c2cf84a31'

actual_head="$(git rev-parse HEAD)"
printf 'HEAD=%s\n' "$actual_head"
if [ "$actual_head" != "$expected_head" ]; then
  printf 'ERROR: expected exact head %s\n' "$expected_head" >&2
  exit 2
fi

printf '\n== Repository status and changed paths ==\n'
git status --short
git diff --name-status "$base...$expected_head"
printf '\n== Diff check ==\n'
git diff --check "$base...$expected_head"

printf '\n== Exact-head delta from superseded head ==\n'
git diff --name-status 'b8526736db3bb16ed88290ca64fd07cf54232805...$expected_head'
git diff --unified=20 'b8526736db3bb16ed88290ca64fd07cf54232805...$expected_head'

printf '\n== Coverage and protected-integration contract ==\n'
sed -n '60,95p' pyproject.toml
rg -n -C 4 -F 'protected-main or organization-level reusable workflow' \
  docs/sbom-release-evidence.md tests/test_quality_contracts.py

printf '\n== Archive generator outline ==\n'
ast-grep outline scripts/ci/generate_release_sbom.py --items all --type function --view expanded
printf '\n== Archive-boundary test outline ==\n'
ast-grep outline tests/test_release_sbom_member_enumeration_bound.py --items all --type function --view expanded

printf '\n== Archive controls and parser call sites ==\n'
rg -n -C 5 -P '\b(ZipFile|infolist|getmembers|tarfile\.open|r:gz|r\|gz|zlib\.error|_preflight_(wheel|sdist)_members|MAX_ARCHIVE_MEMBERS|MAX_EXPANDED_TAR_BYTES|MAX_TAR_EXTENSION_BYTES|_read_expanded|_sha256_file|_require_live_artifact_descriptor)\b' \
  scripts/ci/generate_release_sbom.py \
  tests/test_release_sbom_member_enumeration_bound.py

printf '\n== Full target diff ==\n'
git diff --find-renames --find-copies --unified=30 "$base...$expected_head" -- \
  CHANGELOG.md \
  docs/sbom-release-evidence.md \
  pyproject.toml \
  scripts/ci/generate_release_sbom.py \
  tests/test_release_sbom_member_enumeration_bound.py \
  tests/test_quality_contracts.py

printf '\n== Workflow and sensitive-path changes ==\n'
git diff --name-only "$base...$expected_head" -- .github
git diff --name-only "$base...$expected_head" | rg -n '(^|/)(credentials?|secrets?|.*\.pem|.*\.key)$' || true

printf '\n== Exact-head PR state and workflow runs ==\n'
gh pr view 70 --json headRefOid,isDraft,mergeStateStatus,statusCheckRollup,url
gh run list --branch security/bound-archive-member-enumeration --limit 20 \
  --json databaseId,headSha,name,status,conclusion,event,url,createdAt

Length of output: 3404


🏁 Script executed:

#!/bin/bash
set -euo pipefail

expected_head='56c9029588b67f329c334b4e56e1361087f8f651'
base='224e6d253a7957e981515ec66260fb1c2cf84a31'
superseded_head='b8526736db3bb16ed88290ca64fd07cf54232805'

test "$(git rev-parse HEAD)" = "$expected_head"

printf '\n== Exact-head delta from superseded head ==\n'
git diff --name-status "${superseded_head}...${expected_head}"
git diff --unified=20 "${superseded_head}...${expected_head}" -- \
  docs/sbom-release-evidence.md \
  CHANGELOG.md \
  pyproject.toml \
  scripts/ci/generate_release_sbom.py \
  tests/test_quality_contracts.py \
  tests/test_release_sbom_member_enumeration_bound.py

printf '\n== Coverage and protected-integration contract ==\n'
sed -n '60,95p' pyproject.toml
rg -n -C 5 -F 'protected-main or organization-level reusable workflow' \
  docs/sbom-release-evidence.md tests/test_quality_contracts.py

printf '\n== ZIP preflight and descriptor controls ==\n'
sed -n '55,175p;215,355p' scripts/ci/generate_release_sbom.py

printf '\n== Tar preflight and sequential semantic parser ==\n'
sed -n '345,555p;750,890p' scripts/ci/generate_release_sbom.py

printf '\n== Unsafe or unbounded archive API call sites ==\n'
rg -n -C 5 -P '\b(getmembers|infolist|ZipFile|tarfile\.open|r:gz|r\|gz|zlib\.error|\.read\s*\(\s*\)|\.read\s*\(\s*-1\s*\))' \
  scripts/ci/generate_release_sbom.py \
  tests/test_release_sbom_member_enumeration_bound.py

printf '\n== Boundary-test assertions ==\n'
rg -n -C 10 -P \
  'def test_(new_archive_bounds_are_exact|wheel_member_bound_precedes_zipfile_materialization|sdist_member_bound_does_not_materialize_getmembers|canonical_wheel_and_sdist_remain_compatible|tar_preflight_normalizes_gzip_errors_and_rewinds|tar_extension_and_expansion_limits_precede_semantic_parser)' \
  tests/test_release_sbom_member_enumeration_bound.py

printf '\n== Changed workflow and sensitive paths ==\n'
git diff --name-only "$base...$expected_head" -- .github
git diff --name-only "$base...$expected_head" | rg '(^|/)(credentials?|secrets?|.*\.pem|.*\.key)$' || true

printf '\n== PR state and exact-head runs ==\n'
gh pr view 70 --json headRefOid,isDraft,mergeStateStatus,statusCheckRollup,url
gh run list --branch security/bound-archive-member-enumeration --limit 30 \
  --json databaseId,headSha,name,status,conclusion,event,url,createdAt

Length of output: 53131


🏁 Script executed:

#!/bin/bash
set -euo pipefail

expected_head='56c9029588b67f329c334b4e56e1361087f8f651'
test "$(git rev-parse HEAD)" = "$expected_head"

printf '== Zero-block handling in implementation and tests ==\n'
rg -n -C 12 -P \
  '(zero_headers|b"\\x00" \* 512|b"\\x00" \* 1024|trailing|end-of-archive|end of archive|physical header)' \
  scripts/ci/generate_release_sbom.py \
  tests/test_release_sbom_member_enumeration_bound.py \
  docs/sbom-release-evidence.md

printf '\n== Tar fixture helpers and malformed-input tests ==\n'
sed -n '120,370p' tests/test_release_sbom_member_enumeration_bound.py

printf '\n== Semantic parsing exception and EOF paths ==\n'
sed -n '455,540p' scripts/ci/generate_release_sbom.py

printf '\n== Relevant Python tarfile documentation or installed stdlib source ==\n'
python3 - <<'PY'
import inspect
import tarfile
print(inspect.getsource(tarfile.TarFile.next))
PY

Length of output: 35238


@seonghobae I reviewed exact head 56c9029588b67f329c334b4e56e1361087f8f651 against exact base 224e6d253a7957e981515ec66260fb1c2cf84a31.

I found one issue.

  • scripts/ci/generate_release_sbom.py Line 392 resets zero_headers when a nonzero header follows one zero tar block. This accepts a physical tar stream with one zero block followed by another member. tarfile treats that zero block as the end of the archive, so sequential semantic parsing stops there. If a valid PKG-INFO occurs before the zero block, later malformed or unsafe records are not semantically checked. Reject a nonzero physical header after the first zero block. Add a regression for this form. This preserves the documented fail-closed behavior for malformed tar archives.

I confirmed these items by static inspection:

  • The current head changes only the documentation line wrap from the superseded head.
  • The exact phrase protected-main or organization-level reusable workflow is now contiguous.
  • ZIP central-directory preflight occurs before zipfile.ZipFile.
  • gzip/tar preflight occurs before sequential tarfile.open(..., mode="r|gz").
  • The implementation enforces the 10,000-member, 512 MiB expanded-tar, and 1 MiB extension-header limits.
  • The implementation does not call TarFile.getmembers().
  • zlib.error normalizes to the stable gzip-tar error, and the preflight rewinds the stream.
  • Descriptor identity checks, bounded reads, digest bracketing, and deterministic JSON output remain present.
  • No workflow, credential, or release-control path changed.
  • PR #70 remains a draft.

No CI workflow run targets this exact head. Earlier failed CI runs apply only to earlier heads. I did not infer exact-head CI success.

⚠️ Action not completed

Review rate limited.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown
Contributor Author

@opencode-agent Fix the exact-current-head CodeRabbit finding on PR #70 test-first, and do nothing if the branch head is no longer 56c9029588b67f329c334b4e56e1361087f8f651.

The valid finding is in exact-head review: scripts/ci/generate_release_sbom.py currently resets zero_headers when a nonzero tar header follows one zero 512-byte header. Python tarfile treats that first zero header as end-of-archive, so physical preflight can accept later records that semantic r|gz parsing never sees. This creates a fail-open semantic-validation gap.

Use two auditable commits in this order:

  1. Immutable RED test-only commit. Modify tests only. Add a focused regression that constructs a gzip tar containing a valid root PKG-INFO, then exactly one 512-byte zero header, then a syntactically valid regular-file header for an unsafe member such as ../outside, followed by the normal final zero terminator. Assert the standalone generator fails closed with the stable release source distribution is not a valid gzip tar boundary. The regression must demonstrate the current implementation accepts/skips the post-zero member because tarfile stops at the first zero block. Keep the fixture finite and deterministic. Do not weaken any existing assertion.

  2. GREEN bounded repair. After proving the RED behavior, minimally change _preflight_sdist_members(...) so any nonzero physical header after the first zero tar header is rejected immediately; do not reset the end-marker state and continue. Preserve the two-zero canonical termination requirement, bounded expanded reads, archive/member limits, zlib/OSError/EOF normalization, stream rewind, sequential semantic parser, exact error boundaries, stable API, and all other behavior. Update docs/sbom-release-evidence.md to state that once tar termination begins, subsequent nonzero records are malformed and rejected before semantic parsing. Add one concise [Unreleased] Security entry to CHANGELOG.md; do not bump the version. Keep or add only authoritative primary references in the repository’s existing APA 7 style if the documentation claim needs one.

Allowed paths only: tests/test_release_sbom_member_enumeration_bound.py (or one new focused test file under tests/), scripts/ci/generate_release_sbom.py, docs/sbom-release-evidence.md, CHANGELOG.md. Do not modify .github, workflows, dependencies, credentials, permissions, protected refs, review-agent identities, release/tag/publication behavior, or any other path. Do not rebase, retarget, force-update, merge, mark ready, enable auto-merge, release, or publish.

After GREEN, run the focused regression plus the repository’s complete Python 3.10–3.13-compatible test/quality contract, Ruff, compileall, public-docstring contract, 100% production statement and branch coverage, and package acceptance available locally. Keep PR #70 draft because its stacked base has diverged. Report exact RED and GREEN SHAs in the PR conversation, and request a fresh CodeRabbit review bound to the GREEN SHA without claiming missing hosted checks as success.

Copy link
Copy Markdown
Contributor Author

@opencode-agent Implement the newly confirmed CodeRabbit finding test-first on exact current head 56c9029588b67f329c334b4e56e1361087f8f651 of draft branch security/bound-archive-member-enumeration; stop without writing if the head differs.

The exact-head review found one valid fail-closed tar-boundary defect: the physical gzip/tar preflight resets zero_headers when a nonzero 512-byte header follows one all-zero tar block. Python tarfile treats the first zero block as archive termination, so the later record is not reached by semantic streaming. A valid root PKG-INFO before that zero block can therefore let later malformed/unsafe physical records escape semantic validation. The physical validator must reject any nonzero header after the first zero block instead of resetting the counter.

Required RED → GREEN sequence:

  1. First commit a focused immutable regression in tests/test_release_sbom_member_enumeration_bound.py constructing a gzip sdist whose valid root PKG-INFO is followed by exactly one zero tar block and then a nonzero member/header. Prove current behavior incorrectly accepts or reaches semantic success rather than failing closed. Include a hostile later record shape where useful to demonstrate why accepting post-terminator data is unsafe.
  2. Then modify only scripts/ci/generate_release_sbom.py as narrowly as possible so _preflight_sdist_members (or the exact physical preflight owning this state) treats the first zero block as termination state and rejects every later nonzero physical header with the existing stable invalid-gzip/tar boundary. Preserve the valid canonical two-zero-block end marker, finite expanded-byte/member/extension bounds, tar checksum/numeric validation, link/device/sparse rejection, root PKG-INFO semantics, zlib-error normalization, deterministic SBOM output, descriptor binding, and existing public behavior.
  3. Update docs/sbom-release-evidence.md only if the implemented operator contract needs an explicit canonical-termination statement, and prepend one concise [Unreleased] Security entry only if the existing entry does not already truthfully cover this exact rejection. Do not bump a version.
  4. Run the focused regression first, then the full supported Python 3.10–3.13 suite, Ruff, compileall, public-docstring contract, 100% production statement/branch coverage, and package acceptance on the resulting exact head. Hosted CI/security/review evidence must still be fresh for that final head.

Do not modify .github, workflows, credentials, dependencies, release/tag/ref behavior, signing/attestation, unrelated archive policy, or stacked-base history. Keep the PR Draft; do not merge, mark Ready, enable auto-merge, publish, rebase, retarget, or resolve any review thread. The prior zlib thread is already resolved and must remain historical evidence.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant